home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Scene 96
/
Scene 96 International Edition (Zyklop Software) (Disc 2) (1997).iso
/
misc
/
coding
/
vgacodng
/
part04.txt
< prev
next >
Wrap
Text File
|
1996-08-07
|
16KB
|
359 lines
VGA-Kurs - Part #4
Wir kommen nun zu "T.C.P.'s Beginner's Guide to VGA Coding", Part IV.
Noch eine Anmerkung: Ich versuche immer, auch Lesern, die die ersten Teile des
Kurses verpaßt haben, die Möglichkeit zu geben, die Beispiele zu nutzen, auch
wenn ihnen die wichtigen Prozeduren fehlen. Der Nachteil ist, daß ich z.B.
die schnelle PutPixel-Prozedur durch einen langsameren Speicherzugriff via
Mem ersetzen muß.
Nun eine Frage: Soll ich es so beibehalten oder in jedem Teil die benötigten
Prozeduren dazuschreiben?
Für alle, die die ersten Teile des Kurses haben: Ihr solltet auf jeden Fall
folgende Prozeduren immer bereit halten:
PutPixel, WaitRetrace, ClrVGA. Ihr könnt diese Prozeduren dann in die Listings
einsetzen.
In diesem Teil werden wir Sprites behandeln, als Bonus gibt es einen kleinen
Abschnitt über Code-Optimierung.
Auf dem PC sind Sprites immer noch für viele ein Mythos, die vorher auf
Homecomputern wie Amiga/Atari/C64 gecodet haben, wo die Maschine alle
Sprite-Operationen erledigt.
Beim PC dagegen war am Anfang nie vorgesehen, daß er einmal auch nur ein
Sprite über den Bildschirm flitzen lassen würde. Deshalb baute man solch eine
Funktion auch nie in die PC-Grafikkarten ein. Das Ergebnis ist, daß wenn man
auf der VGA-Karte Sprites zaubern will, sich um alles selbst zu kümmern hat:
Darstellung, Bewegung, Kollisionsabfragen, Clipping, Durchscheinen des
Hintergrundes, runde Sprites etc.
Das dies erheblich auf die CPU geht, ist abzusehen, denn die VGA-Karte trägt
kein bischen dazu bei.
Allerdings kann man sich einiger Tricks behelfen, wie wir noch sehen werden.
Zu allererst sollte man sich einen Sprite-Editor zulegen. Davon gibt es
unzählige als Shareware, man kann aber auch ein Malprogram wie DPaint
heranziehen.
Nun muß man die Sprites in ein Format bekommen, das man von Pascal aus leicht
lesen kann. Hierbei ist es günstig, wenn man einen Sprite-Editor hat, der die
Sprites als RAW-Dump abspeichert. D.h., daß die Pixel-Informationen
hintereinander in einer Datei abgelegt werden. Zeichnet man z.B. ein 32x32
Pixel großes Sprite, so hat man eine 1024 Byte große Datei, die man direkt
einlesen kann:
type Sprite = array[0..1023] of byte;
var f : file;
Spr : Sprite;
procedure Readsprite(str:string;s:Sprite);
begin
assign(f,str);
reset(f,1);
blockread(f,s,1024);
close(f);
end;
Diese Prozedur liest durch die Blockread-Prozedur 1024 Byte aus der Datei mit
dem Namen Str in die übergebene Variable des Typs Sprite.
Anmerkung: Wer den Autodesk Animator besitzt, kann die Sprites im CEL-Format
speichern. Dieses hat allerdings noch einen 800 Byte großen Header, der
per "seek(f,800)" übersprungen werden muß.
Es gibt aber auch Sprite-Editoren, die die Sprites gleich als Pascal-Source
speichern können (z.B. YC's Sprite Editor, den ich benutze). Dies hat dann
die folgende Form:
const Spr : Sprite = (
0,1,2,3,5,128,255,200,50,20,...
);
Nun, da wir hoffentlich unser Sprite in der Variablen Spr haben, wird es Zeit,
es auf den Bildschirm zu bringen:
procedure ShowSprite(x,y:word;s:Sprite);
var n1,n2 : byte;
begin
for n1 := 0 to 31 do
for n2 := 0 to 31 do mem[$A000:(n1+y)*320+x+n2] := s[n1*32+n2];
end;
Dies ist die einfachste Form der ShowSprite-Routine. Kein Clipping, kein
Durchscheinen, keine runden Sprites und das Wichtigste: Keine Geschwindigkeit!
Hier die Funktionsweise der Prozedur: In der Schleife wird ein 32x32 Pixel
großes Sprite an die Koordinaten (X,Y) gesetzt. Dazu wird die Adresse im
Bildschirmspeicher nach der bekannten Formel Adresse = 320 * Y + X berechnet
und dann die Position im Sprite-Array mittels Position = 32 * n1 + n2
dazugezählt. Daraus ergibt sich die Formel Adresse = (n1+Y) * 320 + X + n2.
Schön und gut, aber was ist, wenn das Sprite rund ist, z.B. ein Fußball.
Wenn ich das Sprite auf einen schwarzen Hintergrund setze, macht es nichts,
aber wenn ich einen Hintergrund, wie z.B. ein Fußballfeld habe, und das Sprite
darauf setze, dann habe ich um den Ball einige schwarze Pixel. Das liegt
daran, daß das Array, in dem das Sprite liegt, quadratisch ist, und wenn nun
im Sprite zwar kein Pixel vorgesehen ist, also im Array eine 0 steht, dann
wird trotzdem ein Pixel der Farbe 0 gesetzt.
Noch ein Problem: Hat man ein Sprite, daß an gewissen Stellen durchsichtig
ist, z.B. eine Scheibe Schweizer Käse (Scheiß Beispiel, ich weiß), dann sollte
an den Stellen, wo die Löcher sind, der Hintergrund durchscheinen, tut er aber
nicht, da wieder ein Pixel der Farbe 0 gesetzt wird.
Diese Probleme sind noch leicht zu lösen:
procedure ShowSprite(x,y:word;s:Sprite);
var n1,n2 : byte;
begin
for n1 := 0 to 31 do
for n2 := 0 to 31 do
if s[n1*32+n2] <> 0 then mem[$A000:(n1+y)*320+x+n2] := s[n1*32+n2];
end;
Findet die Routine nun im Sprite ein Pixel der Farbe 0, so wird es erst gar
nicht gesetzt. Dadurch wird die Prozedur sogar schneller, da einige Pixel des
Sprites übersprungen werden, und so Zeit gespart wird.
Ein weiteres Problem ist, daß wenn das Sprite am Rand des Bildschirms
angelangt ist und über den Rand hinausgeht, es an der anderen Seite des
Bildschirms dargestellt wird. Das liegt am linearen Aufbau des
Bildschirmspeichers. Gehen die Spriteinformationen über den Rand einer Zeile
hinaus, werden sie im Bildschirmspeicher weiter bewegt und erscheinen dadurch
in der nächsten Zeile. Dasselbe passiert, wenn das Sprite den unteren Rand des
Bildschirms überschreitet.
Ein Verfahren zum Vereiteln dieser Tatsache ist, daß vor dem Setzen eines
Pixels überprüft wird, ob er überhaupt noch in der Zeile bzw. Spalte liegt,
also X nicht größer als 319 und Y nicht größer als 199 ist. Dies nennt sich
Clipping.
procedure ShowSprite(x,y:word;s:Sprite);
var n1,n2 : byte;
begin
for n1 := 0 to 31 do
for n2 := 0 to 31 do
if (s[n1*32+n2] <> 0) and (n2+x < 320) and (n1+y < 200) then
mem[$A000:(n1+y)*320+x+n2] := s[n1*32+n2];
end;
Damit enthält unsere Prozedur schon eine Menge Ifs und ist deshalb nicht
gerade als die schnellste zu bezeichnen.
Anders wäre das schon bei der Assembler-Version, die zieht sich allerdings
etwas in die Länge...
procedure ShowSprite(x,y,add:word);assembler;
asm
mov bx,31 { Zähler mit Endwert initialisieren }
@loop1: { für ein 32x32 Pixel Sprite }
mov cx,31
@loop2:
mov si,bx { n1 * 32 + n2 }
shl si,5 { si * 32 }
add si,cx
add si,add { Variablen-Offset drauf }
cmp byte ptr ds:[si],0 { Pixelwert = 0 ? }
je @next { Wenn ja, nächster Pixel }
mov ax,cx
add ax,x
cmp ax,319 { X-Koordinate > 319 ? }
ja @next { Wenn ja, nächster Pixel }
mov ax,bx
add ax,y
cmp ax,199 { Y-Koordinate > 199 ? }
ja @next { Wenn ja, nächster Pixel }
mov ax,bx { (n1+y) * 320 + x + n2 }
add ax,y
mov dx,ax
shl ax,6 { ax * 64 }
shl dx,8 { dx * 256 }
add ax,dx
add ax,x
add ax,cx
mov di,ax
mov ax,0A000h { VGA-Segment nach ES }
mov es,ax
mov al,ds:[si] { Pixelwert aus Sprite-Daten holen }
mov es:[di],al { und auf VGA-Screen setzen }
@next:
dec cx { Zähler dekrementieren }
jnz @loop2 { Wenn ungleich 0, innere Schleife }
dec bx { Zähler dekrementieren }
jnz @loop1 { Wenn ungleich 0, äußere Schleife }
end;
Diese Routine schafft immerhin schon rund 3000 Sprites pro Sekunde auf einem
DX/2 80, also ca. doppelt so viel wie die Pascal-Version.
Aber es geht (natürlich) noch schneller.
Die Vorgehensweise der Routine entnehmt ihr bitte den Kommentaren.
Folgende Tricks wurden in dieser Routine zur Optimierung verwendet:
1. Es wird nur das Offset des Sprites übergeben.
Statt das gesamte Sprite als Array an die Prozedur zu übergeben, wird ihr
nur das Offset des Sprites im Speicher mitgeteilt. Dadurch spart man es sich,
ganze 1022 Byte mehr zu übergeben.
Der Prozeduraufruf muß also nicht 'showsprite(x,y,Spritename)' lauten,
sondern 'showsprite(x,y,ofs(Spritename))'.
2. Es werden so viel Register wie möglich ausgenutzt.
Dies ist eine goldene Regel in der Assembler-Programmierung. Man sollte erst
auf Variablen zurückgreifen, wenn wirklich alle Register ausgenutzt sind.
3. Die Zähler werden dekrementiert.
Am Anfang werden die Zähler nicht mit dem Start- sondern mit dem Endwert
initialisiert. Danach werden sie in jeder Schleife dekrementiert. Dies hat
den Vorteil, das man sich ein langsames 'cmp zaehler,Endwert' erspart. Es
genügt der bedingte Sprungbefehl 'jnz', der automatisch erneut zum Anfang der
Schleife springt, wenn der Zähler ungleich 0 ist.
4. Statt 'mul' oder 'div' kommt 'shl' oder 'shr'.
Hat man eine Multiplikation bzw. Division mit bzw. durch einen Faktor der
ein vielfaches von 2 darstellt, kann man die Operation durch Bitverschiebung
beschleunigen. OK, wer hat den Satz verstanden? Niemand? Gut. Also:
Angenommen, man muß (wie in der Prozedur) eine Zahl mit 32 multiplizieren.
Dies geht vor allem auf Prozessoren unter 486 recht träge vonstatten.
Schneller geht es, wenn man die Zahl um 5 nach links shiftet, d.h. alle Bits
der Zahl um 5 Stellen nach links verschiebt. Denn: Eine Verschiebung um 1 Bit
erzeugt dasselbe Ergebnis wie eine Multiplikation mit 2. Ein Shiften um 2
Bits ist wie ein Malnehmen mit 4, 3 Bits wie mal 8, 4 Bits wie mal 16, usw.
Andersrum geht's auch. Ein Shiften um 1 Bit nach rechts ist wie eine Division
durch 2, wobei immer abgerundet wird.
Andere Tricks:
1. Exklusiv-Oder geschickt einsetzen.
Ein 'mov ax,0' ist identisch mit 'xor ax,ax', bloß mit dem Unterschied, daß
die zweite Art wesentlich schneller geht und als OpCode weniger Bytes
verbraucht.
2. So viel wie möglich raus aus der Schleife.
In Schleifen sollte nur das stehen, was dort auch wirklich hingehört. Wenn
Schleifen unnötige Befehle wie Variablenzuweisungen oder Rechenoperationen
enthalten, die auch außerhalb der Schleife ihren Dienst verrichten können,
sollte man sie rauswerfen.
3. Compiler-Optionen ausnutzen.
Folgende Compiler-Befehle am Anfang des Codes beschleunigen das Programm:
{$G+,D-,I-,Q-,R-,S-}
Damit wird alles, was bremst, abgeschaltet: Debug-Informationen,
In/Out-, Range- und Stack-Checking.
So, schön und gut, aber was ist, wenn wir unser Sprite über einen Hintergrund
bewegen wollen? Also, wir setzen das Sprite auf unseren Hintergrund und
löschen es wieder, um es danach an eine andere Stelle zu setzen.
Wuups, da ist ja jetzt ein Loch in unserem schönen Hintergrund!
Tja, um dies zu lösen, könnte man sich Methode 1 oder 2 bedienen. Methode 1
ist, den Hintergrund, auf den das Sprite gesetzt wird, vor dem Setzen zu
sichern, und danach wieder herzustellen, wie in folgendem Schema:
var Buffer : Sprite;
n1,n2 : byte;
begin
setmcgamode;
ErzeugeHintergrund;
repeat
for n1 := 0 to 31 do { Ausschnitt sichern }
for n2 := 0 to 31 do
Buffer[n1*32+n2] := mem[$A000:(n1+y)*320+x+n2];
showsprite(x,y,ofs(MySprite)); { Sprite zeichnen }
showsprite(x,y,ofs(Buffer)); { HG wiederherstellen }
BewegeSprite; { Neue Koordinaten }
until Bedingung = true;
settextmode;
end.
Gut, für ein Sprite mag diese Methode ausreichen, aber was ist, wenn man z.B.
10 Sprites über den HG bewegen will? Tja, jetzt muß Methode 2 herhalten.
Diese benutzt folgendes Schema:
begin
setmcgamode;
ErzeugeHintergrundAufVS;
repeat
KopiereHGAufVGA;
ZeichneSprites;
until Bedingung = true;
settextmode;
end.
Moment maaal! Was soll den 'VS' sein? Nun, das ist der Virtual Screen. Also
ein 'virtueller Bildschirm'. Man kann ihn beschreiben und auslesen wie die
VGA. Aber das Wichtige ist: Man kann ihn auf die VGA kopieren, so, daß sein
Inhalt auf dem Bildschirm erscheint.
Man erstellt also einen Virtual Screen (wie, werden wir noch sehen), und
erstellt einen Hintergrund auf ihm. Dann startet man die Schleife und kopiert
den Hintergrund aus dem VS auf die VGA. Nun zeichnet man seine Sprites darauf.
Aber wie erstelle ich nun so ein Teil???
Am einfachsten wäre wohl die folgende Lösung:
var VS : array[0..63999] of byte;
Diese Variable könnte nun wie ein zweiter VGA-Bildschirm benutzt werden, aber:
Das Array ist 64000 Byte groß. Da man aber unter Pascal für globale Variablen
nur maximal 65000 und ein paar Byte zur Verfügung hat, würde es sehr eng
werden, und schnell kriegen wir die Compiler-Meldung 'Zuviele Variablen'.
Die Lösung für unser Problem sind also: Zeiger (Pointer).
Die Deklaration des VS muß also so aussehen:
type BigArr = array[0..63999] of byte;
VSPtr = ^BigArr;
var VS : VSPtr;
VSAdd : word;
Nun müssen wir den VS nur noch initialisieren:
procedure InitVS;
begin
getmem(VS,64000);
VSAdd := seg(VS^);
end;
Nun haben wir für den VS 64000 Byte an Speicher allokiert. Um ihn wieder
freizugeben sollte folgende Prozedur benutzt werden:
procedure CloseVS;
begin
freemem(VS,64000);
end;
Will man nun auf den VS statt auf die VGA schreiben, ersetzt man die Adresse
des Bildschirmspeichers 'A000h:0' durch 'VSAdd:0'. Mit dem Mem-Befehl sähe das
so aus:
x := 50;
y := 80;
mem[VSAdd:y*320+x] := Col;
setzt auf dem VS an die Koordinaten (50,80) einen Pixel der Farbe Col.
Was aber nützt mir das? Nun, nachdem wir den Hintergrund auf diese Weise auf
den VS gezeichnet haben, können wir ihn mit der folgenden Prozedur auf den
VGA-Screen kopieren:
procedure Flip;assembler;
asm
push ds { DS MUß gesichert werden }
mov ax,0A000h { Zielsegment nach ES }
mov es,ax
mov ds,VSAdd { Quellsegment nach DS }
xor si,si { Offset = 0 }
xor di,di
mov cx,32000 { 32000 Word (=64000 Byte) }
rep movsw { kopieren }
pop ds { DS wiederherstellen }
end;
Nun werden, wie besprochen, die Sprites darüber gezeichnet, was natürlich
entsprechend flott vonstatten gehen sollte. Um Flickern und 'zerrissene'
Sprites zu vermeiden, sollte man außerdem noch die WaitRetrace-Prozedur
aufrufen.
Wer das mit dem Virtual Screen noch nicht ganz gepeilt hat (ist nicht einfach,
geb ich zu), der sei getröstet, ab der nächsten Ausgabe brauchen wir so was
nicht mehr, denn dann besprechen wir den VGA-Modus, der von Hause aus
4 Virtual Screens mitbringt, ohne auch nur ein Byte mehr Speicher zu
verbrauchen: Der Mode-X!
Also, ciao bis zum nächsten Teil.
[ This text copyright (c) 1995-96 Johannes Spohr. All rights reserved. ]
[ Distributed exclusively through PC-Heimwerker, Verlag Thomas Eberle. ]
[ ]
[ No part of this document may be reproduced, transmitted, ]
[ transcribed, stored in a retrieval system, or translated into any ]
[ human or computer language, in any form or by any means; electronic, ]
[ mechanical, magnetic, optical, chemical, manual or otherwise, ]
[ without the expressed written permission of the author. ]
[ ]
[ The information contained in this text is believed to be correct. ]
[ The text is subject to change without notice and does not represent ]
[ a commitment on the part of the author. ]
[ The author does not make a warranty of any kind with regard to this ]
[ material, including, but not limited to, the implied warranties of ]
[ merchantability and fitness for a particular purpose. The author ]
[ shall not be liable for errors contained herein or for incidental or ]
[ consequential damages in connection with the furnishing, performance ]
[ or use of this material. ]